﻿using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace Devonline.AspNetCore;

/// <summary>
/// 数据库上下文扩展操作集合
/// </summary>
public static class DbContextExtensions
{
    /// <summary>
    /// 自动迁移
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public static async Task AutoMigrationAsync(this DbContext context)
    {
        if ((await context.Database.GetPendingMigrationsAsync()).Any())
        {
            await context.Database.MigrateAsync();
        }
    }

    /// <summary>
    /// 递归获取父级数据 
    /// </summary>
    /// <typeparam name="TEntitySet">业务数据类型</typeparam>
    /// <typeparam name="TKey">主键类型</typeparam>
    /// <param name="context">数据库上下文</param>
    /// <param name="id">当前业务数据编号</param>
    /// <returns></returns>
    public static async Task<List<TEntitySet>> GetParentsAsync<TEntitySet, TKey>(this DbContext context, TKey id) where TKey : IConvertible, IEquatable<TKey> where TEntitySet : class, IParent<TKey>, IEntitySetWithCreate<TKey>
    {
        var entitySets = new List<TEntitySet>();
        var entitySet = await context.Set<TEntitySet>().FindAsync(id);
        if (entitySet != null && entitySet.ParentId != null)
        {
            var parent = await context.Set<TEntitySet>().FirstOrDefaultAsync(x => x.State == DataState.Available && x.Id.Equals(entitySet.ParentId));
            if (parent != null)
            {
                entitySets.Add(parent);
                entitySets.AddRange(await context.GetParentsAsync<TEntitySet, TKey>(parent.Id));
            }
        }

        return entitySets;
    }
    /// <summary>
    /// 递归获取子级数据 
    /// </summary>
    /// <typeparam name="TEntitySet">业务数据类型</typeparam>
    /// <typeparam name="TKey">主键类型</typeparam>
    /// <param name="context">数据库上下文</param>
    /// <param name="id">当前业务数据编号</param>
    /// <returns></returns>
    public static async Task<List<TEntitySet>> GetChildrenAsync<TEntitySet, TKey>(this DbContext context, TKey id) where TKey : IConvertible, IEquatable<TKey> where TEntitySet : class, IParent<TKey>, IEntitySetWithCreate<TKey>
    {
        var entitySets = new List<TEntitySet>();
        if (id != null)
        {
            var children = await context.Set<TEntitySet>().Where(x => x.State == DataState.Available && x.ParentId != null && x.ParentId.Equals(id)).ToListAsync();
            if (children.IsNotNullOrEmpty())
            {
                entitySets.AddRange(children);
                foreach (var child in children)
                {
                    entitySets.AddRange(await context.GetChildrenAsync<TEntitySet, TKey>(child.Id));
                }
            }
        }

        return entitySets;
    }

    /// <summary>
    /// 递归获取父级数据, 字符串作为主键的默认实现
    /// </summary>
    /// <typeparam name="TEntitySet">业务数据类型</typeparam>
    /// <param name="context">数据库上下文</param>
    /// <param name="id">当前业务数据编号</param>
    /// <returns></returns>
    public static async Task<List<TEntitySet>> GetParentsAsync<TEntitySet>(this DbContext context, string id) where TEntitySet : class, IParent<string>, IEntitySetWithCreate<string> => await context.GetParentsAsync<TEntitySet, string>(id);
    /// <summary>
    /// 递归获取子级数据, 字符串作为主键的默认实现
    /// </summary>
    /// <typeparam name="TEntitySet">业务数据类型</typeparam>
    /// <param name="context">数据库上下文</param>
    /// <param name="id">当前业务数据编号</param>
    /// <returns></returns>
    public static async Task<List<TEntitySet>> GetChildrenAsync<TEntitySet>(this DbContext context, string id) where TEntitySet : class, IParent<string>, IEntitySetWithCreate<string> => await context.GetChildrenAsync<TEntitySet, string>(id);

    /// <summary>
    /// 使用 insert into (columns) values (values), (values)... 的语法批量写入数据
    /// 在单次写入数量或字符限制大小某一个达到限制值时即提交一次
    /// </summary>
    /// <typeparam name="T">待写入的数据类型</typeparam>
    /// <typeparam name="TKey">主键类型</typeparam>
    /// <param name="context">数据库上下文</param>
    /// <param name="data">待写入数据</param>
    /// <param name="limit">单次写入限制数量, 默认 1000, 这个值由数据库链接决定</param>
    /// <param name="maxSize">单次写入的字符大小限制, 默认 1M 字符</param>
    /// <param name="quote">字段引用符号, 针对字段名是 数据库关键字的情况下, 每种数据库有不同的引用表示方式, 如 SQLServer: [], MySQL:` , 默认无</param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public static async Task<int> BulkInsertAsync<T, TKey>(this DbContext context, IEnumerable<T> data, int limit = AppSettings.UNIT_THOUSAND, int maxSize = AppSettings.UNIT_MEGA, char? quote = default) where T : class, IEntitySet<TKey>
    {
        var type = typeof(T);
        var propertyInfos = type.GetProperties().Where(x => x.HasAttribute<ColumnAttribute>());
        var separator = quote.HasValue ? quote.Value.ToString() : string.Empty;
        var sqlInsert = $"INSERT INTO {type.GetTableName()} ({string.Join(AppSettings.DEFAULT_SPLITER_STRING, propertyInfos.Select(p => separator + p.GetColumnName() + separator))}) VALUES ";
        var values = new List<string>();
        var index = 0;
        var total = 0;
        var sql = string.Empty;
        foreach (var entity in data)
        {
            var sqls = new List<string>();
            foreach (var propertyInfo in propertyInfos)
            {
                var value = propertyInfo.GetValue(entity);
                var propertyType = propertyInfo.PropertyType.GetCoreType();
                if (value == null && (propertyInfo.PropertyType.HasAttribute<RequiredAttribute>() || (propertyType != typeof(string) && !propertyInfo.PropertyType.IsNullable())))
                {
                    throw new Exception($"The type of {type.GetDisplayName()} has a required property: {propertyInfo.Name} for object id: {entity.Id}");
                }

                //此处包含了枚举类型对字符串的隐射, 如果枚举类型不对应字符串存储, 则会出现异常
                if (value != null && (propertyType == typeof(string) || propertyType == typeof(Guid) || propertyType == typeof(DateTime) || propertyType == typeof(DateTimeOffset) || propertyType.IsEnum))
                {
                    sqls.Add($"'{value}'");
                }
                else
                {
                    sqls.Add(value?.ToString() ?? "NULL");
                }
            }

            index++;
            values.Add($"({string.Join(AppSettings.DEFAULT_SPLITER_STRING, sqls)})");
            if (index >= limit || values.Sum(x => x.Length) >= maxSize)
            {
                sql = sqlInsert + string.Join(AppSettings.DEFAULT_SPLITER_STRING, values);
                total += await context.Database.ExecuteSqlRawAsync(sql);
                values.Clear();
                sqls.Clear();
                index = 0;
            }
        }

        sql = sqlInsert + string.Join(AppSettings.DEFAULT_SPLITER_STRING, values);
        total += await context.Database.ExecuteSqlRawAsync(sql);
        return total;
    }
    /// <summary>
    /// 使用 insert into (columns) values (values), (values)... 的语法批量写入数据
    /// 字符串作为主键的默认实现
    /// </summary>
    /// <typeparam name="T">待写入的数据类型</typeparam>
    /// <param name="context">数据库上下文</param>
    /// <param name="data">待写入数据</param>
    /// <param name="limit">单次写入限制数量, 默认 1000, 这个值由数据库链接决定</param>
    /// <param name="maxSize">单次写入的字符大小限制, 默认 1M 字符</param>
    /// <param name="quote">字段引用符号, 针对字段名是 数据库关键字的情况下, 每种数据库有不同的引用表示方式, 如 SQLServer: [], MySQL:` , 默认无</param>
    /// <returns></returns>
    public static async Task<int> BulkInsertAsync<T>(this DbContext context, IEnumerable<T> data, int limit = AppSettings.UNIT_HUNDRED, int maxSize = AppSettings.UNIT_MEGA, char? quote = default) where T : class, IEntitySet<string> => await context.BulkInsertAsync<T, string>(data, limit, maxSize, quote);
}